查看原文
其他

一个Android沉浸式状态栏上的黑科技

郭霖 郭霖 2022-12-14

说起来,在不知不觉中,我竟然凑成了这沉浸式状态栏三部曲。


其实最开始的时候,我主要是因为工作上的原因想要在Android版的Edge浏览器上实现首页图片沉浸式的功能。

那么为了实现这个功能,我提前去做了一些技术调研,并将调研的结果整理成了一篇文章,具体可参阅 再学一遍android:fitsSystemWindows属性

做完技术调研之后,接下来就是功能实现了。对于Android版的Edge浏览器而言,首页图片的沉浸式一直是部分网友长久以来的呼声,经过我的各种攻坚和踩坑之后,终于将这个功能完成了。具体可参阅 我为Android版Microsoft Edge所带来的变化

实现沉浸式之后的效果如下图所示:


不过,有朋友在评论区提出了这样一个疑问:


确实,这是一个做沉浸式功能时比较容易被忽略的问题。如果背景图片的颜色和状态栏图标的颜色非常接近的话,那么的确会造成状态栏图标看不清楚的情况。

这里我举了一些沉浸式效果做得不太好的案例,具体是什么App我就不提了。



可以看到,这些App虽然实现了沉浸式状态栏的效果,但是由于状态栏上的图标变得难以看清,所以最终效果可能反而不好。

但是,Edge浏览器是不会存在这种问题的。为什么呢?这就是我在上篇文章中说的,在实现沉浸式状态栏时运用了一些小黑科技。那么借助这些小黑科技,我终于可以凑成这沉浸式状态栏三部曲了。

话不多说,下面技术开讲。

其实想要解决上图中的这种由于颜色值接近,导致部分内容看不清的情况,我能想到两种解决方案。一种是从设计层面解决,一种是从技术层面解决。

从设计层面解决相对会比较容易一些,同时应该也是大部分App会采用的方案,那就是在背景图的上方再盖一层阴影。有了这层阴影之后,我们可以让状态栏上的图标始终都是浅色的。即使出现浅色的背景图,由于阴影层的存在,状态栏上的图标依然是可以看得清的。

但如果只是用这个方案解决的话,那么我就不会写本篇文章了。因为这里我们会采用第二种方案,从技术层面解决。

首先从技术层面进行分析,要解决这个问题,无非就是需要将背景图颜色和状态栏图标的颜色区分开。

Android系统其实给了我们API来控制状态栏图标的颜色,但是只能设置成黑、白这两种颜色,而不可以将状态栏图标改成五颜六色的样子。

默认情况下,系统会认为我们拥有的是一个深色的状态栏,那么状态栏上面的图标自然就应该白色的,因为只有这样才能看得清上面的图标。

而调用如下API则可以让系统认为我们拥有的是一个浅色的状态栏:

private fun setLightStatusBar() {
    val flags = window.decorView.systemUiVisibility
    window.decorView.systemUiVisibility = flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}

如此一来,状态栏上面的图标就会变成黑色的,以和浅色的状态栏相互映衬。

如果要动态恢复成默认的深色状态栏,只需要这样设置:

private fun setDarkStatusBar() {
    val flags = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
    window.decorView.systemUiVisibility = flags xor View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}

这就是我们拥有的用于控制状态栏图标颜色的API。

好了,现在有了这个法宝来控制状态栏图标的颜色,那么接下来的问题就是,什么时候应该显示白色的状态栏图标?什么时候应该显示黑色的状态栏图标?

答案是显而易见的,为了能让前景背景的颜色区分更加明显,当然应该是底部是深色背景图的时候显示白色的状态栏图标,底部是浅色背景图的时候显示黑色的状态栏图标。

因此,现在的问题就转移成了,我们如何才能识别一张背景图的指定区域是属于深色还是浅色?

非常幸运,在Android系统上我们是可以做到这一点的,只需要借助Google提供的Palette库即可。

Palette是一个专门用于对图像进行颜色提取和识别的库,功能虽然不能说是非常强大,但是已经完全可以满足我们这里的需求了。

要使用Palette库,首先需要将它引入到项目当中,如下所示:

dependencies {
    implementation 'androidx.palette:palette:1.0.0'
}

接下来我们就可以借助Palette来进行一些颜色提取功能了,示例用法如下:

Palette
    .from(bitmap)
    .setRegion(left, top, right, bottom)
    .maximumColorCount(colorCount)
    .generate {

}

这是Palette最基础、最常见的用法。

首先,我们传入一个bitmap对象,这样Palette就会对它来进行图像解析。

然后调用setRegion()方法来指定解析这个bitmap对象的哪个区域。比方说我们本篇文章是要解决状态栏图标的问题,那肯定就要去解析手机状态栏那个区域的颜色值,其他区域的颜色值对我们来说没有意义。

接着调用maximumColorCount()方法来告诉Palette一共需要提取多少个颜色特征点。具体的颜色提取算法是由Palette自己控制的,我们无需关心。反正只需要知道,最终提取出来的这些颜色值都是这个bitmap的指定区域里最具代表性的就可以了。

最后调用generate()方法开始解析,Palette会开启异步线程来执行解析操作,并将最终结果回调到Lambda表达式当中。

现在我们已经得到这些提取出的特征点颜色值了,那么接下来,我们又该如何处理它们呢?

需要说明的事,后续的处理逻辑其实并没有一个非常严格的规定。我只说一下我个人的处理方式,大家也完全可以去定义自己的处理逻辑。

先贴一下代码,我再进行解释:

Palette
    .from(bitmap)
    .maximumColorCount(colorCount)
    .setRegion(left, top, right, bottom)
    .generate {
        it?.let { palette ->
            var mostPopularSwatch: Palette.Swatch? = null
            for (swatch in palette.swatches) {
                if (mostPopularSwatch == null
                    || swatch.population > mostPopularSwatch.population) {
                    mostPopularSwatch = swatch
                }
            }
            mostPopularSwatch?.let { swatch ->
                val luminance = ColorUtils.calculateLuminance(swatch.rgb)
                // 当luminance小于0.5时,我们认为这是一个深色值.
                if (luminance < 0.5) {
                    setDarkStatusBar()
                } else {
                    setLightStatusBar()
                }
            }
        }
    }

由于刚才在maximumColorCount()方法中传入了提取颜色特征点的数量,因此generate()方法的回调当中我们就可以得到多个颜色特征点(Swatch)。

而每个颜色特征点都会有一个权重值,调用getPopulation()方法可以获取,表示该特征点在选定的bitmap区域的重要程度。我选取了权重值最高的那个特征点来作为这个bitmap区域的代表颜色值。

接下来再调用ColorUtils.calculateLuminance()方法来计算选取的这个颜色值的亮度。当亮度低于0.5时,我就认为这是一个深色的颜色值,那么此时将状态栏设置成深色模式,状态栏图标就会自动变成白色。反之就将状态栏设置成浅色模式,此时状态栏图标就会自动变成黑色。

大概流程就是这个样子,我觉得原理还是非常简单的,我甚至都没有给出一个完整的实例,只是贴出了一些代码片段。

至于Palette,终归只是一个比较小众的库,知道或使用过的人可能并不多,所以用上这种小众技术我觉得足以称得上是黑科技了。

那么最后我们就来看一看实际的运行效果吧。

这里我准备了几张不同的背景图,由Palette解析之后,会根据识别出的颜色值动态更改状态栏图标的颜色。

这是深色背景图的效果。


这是浅色背景图的效果。


可以看到,不管在什么背景图下,状态栏图标的颜色都可以做到自动适配,保证图标始终是清晰可见的。

目前这种使用Palette来动态进行颜色识别的方案,我感觉至少是可以保证99%以上的场景都能够正确适配的,但是也存在一些特别极端的场景。

比如说背景图就是一张黑白左右分割的图片,这种情况下Palette会选取哪种颜色来作为代表色其实是不确定的。但不管是选中了黑还是白,都一定会导致状态栏上有一半区域的图标是不可见的。效果如下图所示:


不过对于这种极端情况,我觉得就没必要强求了。甚至我都并不认为这是一个Bug,反而觉得这是一种很酷的效果,你们觉得呢?

好的,本篇文章就到这里。文中我只帖出了所有关键代码的示例,以及最终运行效果的截图。如果你不想自己动手去敲一遍,也可以直接参考我的完整源码:

https://github.com/guolindev/ImmersiveStatusBar

沉浸式状态栏三部曲到此完结。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
再学一遍android:fitsSystemWindows属性
我为Android版Microsoft Edge所带来的变化

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存